package org.cllib.j2me;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

import javax.microedition.lcdui.Graphics;

/**
 * This class is used to represent a PNG image in case system support is
 * inadequate or with some flaw
 * 
 * 
 * @author liang chen
 * 
 */
public class PNGImage {
	public final static int[] PNG_MAGIC_NUMBER = { 137, 80, 78, 71, 13, 10, 26,
			10 };

	public final static int CHUNK_TYPE_UNKNOWN = -1;
	public final static int CHUNK_TYPE_IHDR = 0;
	public final static int CHUNK_TYPE_PLTE = 1;
	public final static int CHUNK_TYPE_IDAT = 2;
	public final static int CHUNK_TYPE_IEND = 3;

	public final static int[] IHDR = { 73, 72, 68, 82 };
	public final static int[] PLTE = { 80, 76, 84, 69 };
	public final static int[] IDAT = { 73, 68, 65, 84 };
	public final static int[] IEND = { 73, 69, 78, 68 };

	public final static int[][] CHUNK_TYPES = { IHDR, PLTE, IDAT, IEND };

	public final static int IHDR_WIDTH_LENGTH = 4;
	public final static int IHDR_HEIGHT_LENGTH = 4;
	public final static int IHDR_BIT_DEPTH_LENGTH = 1;
	public final static int IHDR_COLOR_TYPE_LENGTH = 1;
	public final static int IHDR_COMPRESSION_METHOD_LENGTH = 1;
	public final static int IHDR_FILTER_METHOD_LENGTH = 1;
	public final static int IHDR_INTERLACE_METHOD_LENGTH = 1;

	public final static int COLOR_TYPE_GREY_SCALE = 0;
	public final static int COLOR_TYPE_TRUE_COLOR = 2;
	public final static int COLOR_TYPE_INDEXED_COLOR = 3;
	public final static int COLOR_TYPE_GREY_SCALE_WITH_ALPHA = 4;
	public final static int COLOR_TYPE_TRUE_COLOR_WITH_ALPHA = 6;

	public final static int[] LEGAL_COLOR_TYPE = { COLOR_TYPE_GREY_SCALE,
			COLOR_TYPE_TRUE_COLOR, COLOR_TYPE_INDEXED_COLOR,
			COLOR_TYPE_GREY_SCALE_WITH_ALPHA, COLOR_TYPE_TRUE_COLOR_WITH_ALPHA };

	public final static int INFLATE_BUFFER_SIZE = 100 * 1024;

	public final static int[][] LEGAL_BIT_DEPTH = {
			{ COLOR_TYPE_GREY_SCALE, 1, 2, 4, 8, 16 },
			{ COLOR_TYPE_TRUE_COLOR, 8, 16 },
			{ COLOR_TYPE_INDEXED_COLOR, 1, 2, 4, 8 },
			{ COLOR_TYPE_GREY_SCALE_WITH_ALPHA, 8, 16 },
			{ COLOR_TYPE_TRUE_COLOR_WITH_ALPHA, 8, 16 }, };

	public final static int PALETTE_COLOR_COMPONENT_LENGTH = 1;

	public final static int UNSIGNED_INT_LENGTH = 4;
	public final static int CHUNK_TYPE_LENGTH = 4;

	private static int tmpWidth, tmpHeight;
	private static int[] tmpPalette;
	private static byte[] tmpData;
	private static int[] tmprgb;

	private int width, height;
	private int[] palette;
	private int[] rgb;

	private final static int COMPRESSION_NONE = 0;
	private final static int COMPRESSION_FIXED_HUFFMAN = 1;
	private final static int COMPRESSION_DYNAMIC_HUFFMAN = 2;
	private final static int COMPRESSION_RESERVED = 3;

	private final static int COMPRESSION_METHOD_DEFLATE = 8;
	private final static int COMPRESSION_METHOD_RESERVED = 15;

	private final static String[] DEFLATE_COMPRESSION_LEVEL_DESCRIPTION = {
			"0 - compressor used fastest algorithm",
			"1 - compressor used fast algorithm",
			"2 - compressor used default algorithm",
			"3 - compressor used maximum compression, slowest algorithm" };

	public static PNGImage createPNGImage(byte[] data, int offset, int length) {
		// System.out.println("creating png image with length " + length +
		// " with offset " + offset);
		int index = offset;
		for (int i = 0; i < PNG_MAGIC_NUMBER.length; i++) {
			if (PNG_MAGIC_NUMBER[offset + i] != (0xff & data[i])) {
				throw new IllegalArgumentException(
						"PNG magic number match fail :" + i
								+ " supposed to be " + PNG_MAGIC_NUMBER[i]
								+ " found :" + data[i]);
			}
		}
		// System.out.println("PNG magic number match ok");
		index += PNG_MAGIC_NUMBER.length;
		while (index < length) {
			// length of chunk four byte unsigned int
			int chunkLength = readInt(data, index);
			// System.out.println("Hex of chunk length :" +
			// Integer.toHexString(chunkLength));
			// System.out.println("chunk length :" + chunkLength);
			index += UNSIGNED_INT_LENGTH;
			// type of chunk four byte
			int chunkType = readInt(data, index);
			index += CHUNK_TYPE_LENGTH;
			// data of chunk can be zero
			byte[] chunkData = new byte[chunkLength];
			System.arraycopy(data, index, chunkData, 0, chunkLength);

			switch (getChunkType(chunkType)) {
			case CHUNK_TYPE_IHDR:
				parseIHDR(chunkData, 0, chunkData.length);
				break;
			case CHUNK_TYPE_PLTE:
				parsePLTE(chunkData, 0, chunkData.length);
				break;
			case CHUNK_TYPE_IDAT:
				parseIDAT(chunkData, 0, chunkData.length);
				break;
			case CHUNK_TYPE_IEND:
				parseIEND(chunkData, 0, chunkData.length);
				break;
			default:
				break;
			}

			index += chunkLength;
			// crc of chunk four byte (type + data)
			int crc = readInt(data, index);
			// System.out.println("crc :" + crc + " last digit :" + (crc &
			// 0x1));

			index += UNSIGNED_INT_LENGTH;
		}

		PNGImage image = new PNGImage(tmpWidth, tmpHeight, tmpPalette, tmpData);
		tmpPalette = null;
		tmpData = null;
		return image;
	}

	private PNGImage(int width, int height, int[] palette, byte[] data) {
		this.width = width;
		this.height = height;
		this.palette = palette;

		this.rgb = new int[width * height];
		for (int i = 0; i < height; i++) {
			for (int j = 0; j < width; j++) {
				rgb[i * width + j] = palette[0xff & data[i * (width + 1) + j
						+ 1]];
			}
		}
	}

	private static int getChunkType(int chunkType) {
		int[] intParts = tearUpInt(chunkType);
		next_chunk: for (int i = 0; i < CHUNK_TYPES.length; i++) {
			for (int j = 0; j < CHUNK_TYPE_LENGTH; j++) {
				if (intParts[j] != CHUNK_TYPES[i][j]) {
					continue next_chunk;
				}
			}
			return i;
		}
		return CHUNK_TYPE_UNKNOWN;
	}

	/**
	 * this method is used to tear up an int into 4 ints in the MSB
	 * 
	 * @param chunkType
	 * 
	 * @return
	 */
	private static int[] tearUpInt(int chunkType) {
		return new int[] { ((chunkType & 0xff000000) >>> 24),
				((chunkType & 0xff0000) >>> 16), ((chunkType & 0xff00) >>> 8),
				((chunkType & 0xff)) };
	}

	/**
	 * parse image info from the IHDR chunk with the format descriped as
	 * following
	 * 
	 * Width 4 bytes Height 4 bytes Bit depth 1 byte Colour type 1 byte
	 * Compression method 1 byte Filter method 1 byte Interlace method 1 byte
	 * 
	 * @param data
	 * @param offset
	 * @param length
	 */
	private static void parseIHDR(byte[] data, int offset, int length) {
		// System.out.println("Parsing head info width length " + length +
		// " offset " + offset);
		int index = offset;

		tmpWidth = readInt(data, index);
		index += IHDR_WIDTH_LENGTH;

		tmpHeight = readInt(data, index);
		index += IHDR_HEIGHT_LENGTH;

		int bitDepth = data[index];
		index += IHDR_BIT_DEPTH_LENGTH;

		int colorType = data[index];
		index += IHDR_COLOR_TYPE_LENGTH;

		int compressionMethod = data[index];
		index += IHDR_COMPRESSION_METHOD_LENGTH;

		int filterMethod = data[index];
		index += IHDR_FILTER_METHOD_LENGTH;

		int interlaceMethod = data[index];
		index += IHDR_INTERLACE_METHOD_LENGTH;

		System.out.println("IHDR width " + tmpWidth + " height " + tmpHeight
				+ " bit depth " + bitDepth + " color type " + colorType
				+ " compression method " + compressionMethod
				+ " filter method " + filterMethod + " interlace method "
				+ interlaceMethod);

		// verify the head info
		boolean legalColorType = false;
		for (int i = 0; i < LEGAL_COLOR_TYPE.length; i++) {
			if (colorType == LEGAL_COLOR_TYPE[i]) {
				legalColorType = true;
				break;
			}
		}
		if (!legalColorType) {
			throw new IllegalArgumentException("color type illegal :"
					+ colorType);
		}

		boolean legalBitDepth = false;
		next_color_type: for (int i = 0; i < LEGAL_BIT_DEPTH.length; i++) {
			if (LEGAL_BIT_DEPTH[i][0] == colorType) {
				for (int j = 1; j < LEGAL_BIT_DEPTH[i].length; j++) {
					if (bitDepth == LEGAL_BIT_DEPTH[i][j]) {
						legalBitDepth = true;
						break next_color_type;
					}
				}
			}
		}

		if (!legalBitDepth) {
			throw new IllegalArgumentException("bit depth illegal :" + bitDepth);
		}
	}

	private static void parsePLTE(byte[] data, int offset, int length) {
		if (length % 3 != 0) {
			throw new IllegalArgumentException("illegal palette length :"
					+ data);
		}
		tmpPalette = new int[length / 3];
		int paletteIndex = 0;
		int index = offset;
		for (int i = index; i < offset + length;) {
			int red = (data[i] & 0xff);
			int green = (data[i + 1] & 0xff);
			int blue = (data[i + 2] & 0xff);
			tmpPalette[paletteIndex++] = (red << 16) | (green << 8) | blue;
			i += PALETTE_COLOR_COMPONENT_LENGTH * 3;
		}
	}

	private static void parseIDAT(byte[] data, int offset, int length) {
		// System.out.println("parsing idat");
		// ZStream inflateStream = new ZStream();

		// inflateStream.next_in = data;
		// inflateStream.next_in_index = 0;

		byte[] buffer = new byte[(tmpWidth + 1) * tmpHeight];

		// inflateStream.next_out = buffer;
		// inflateStream.next_out_index = 0;

		// inflateStream.inflateInit();
		// int code = JZlib.Z_ERRNO;
		// while(true){
		// inflateStream.avail_in = inflateStream.avail_out = buffer.length;
		// code = inflateStream.inflate(JZlib.Z_FINISH);
		// if(code < 0){
		// throw new IllegalArgumentException("Invalid IDAT");
		// }else if (code == JZlib.Z_STREAM_END){
		// break;
		// }
		// }

		// tmpData = new byte[(int)inflateStream.total_out];
		System.arraycopy(buffer, 0, tmpData, 0, tmpData.length);
		buffer = null;

		// deflated data within PNG is stored in the zlib format which has the
		// following
		// structure
		// zlib compression method/flags code 1 byte
		// Additional flags/check bits 1 byte
		// Compressed data blocks n bytes
		// Check value 4 bytes
		if (true)
			return;
		int index = 0;
		int CMF = data[index++] & 0xff;
		int CM = CMF & 0xf;// bits [0-3]
		int CINFO = (CMF & 0xf0) >>> 4;// bits[7-4]
		int lz77WindowSize = CINFO + 8;
		switch (CM) {
		case COMPRESSION_METHOD_DEFLATE:
			System.out.println("deflate compression detected");
			if (lz77WindowSize > 15) {
				throw new IllegalStateException("Invalid LZ77 window size :"
						+ lz77WindowSize);
			}
			System.out.println("LZ77 window size :" + (CINFO + 8));
			break;
		case COMPRESSION_METHOD_RESERVED:
			System.out.println("reserved compression detected");
			break;
		default:
			System.out.println("unrecognized compression detected :" + CM);
			break;
		}

		int FLG = data[index++] & 0xff;

		int CMF_FLG = (CMF << 8 | FLG);
		if (CMF_FLG % 31 != 0) {
			throw new IllegalStateException("Illegal CMF and FLG check value");
		}
		System.out.println("Combination of CMF and FLG :" + CMF_FLG);

		int FCHECK = data[1] & 0x1f;
		int FDICT = (data[1] & 0x20) >>> 5;
		int FLEVEL = (data[1] & 0xc0) >>> 6;

		System.out.println("FLG :" + FLG + " , FCHECK :" + FCHECK
				+ " , FDICT :" + FDICT + " , FLEVEL :" + FLEVEL);

		if (FDICT == 1) {
			System.out.println("DICT is used , skip 4 bytes");
			index += 4;
		}

		if (CM == COMPRESSION_METHOD_DEFLATE) {
			System.out.println("deflate compression level :"
					+ DEFLATE_COMPRESSION_LEVEL_DESCRIPTION[FLEVEL]);
		}

		// now we can begin to decode the DEFLATE format data

		boolean lastBlock = false;
		do {
			System.out.println("current operation byte : "
					+ Integer.toBinaryString(data[index] & 0xff));
			int BFINAL = (data[index] & 0x1);
			if (BFINAL == 1) {
				// last block
				System.out.println("Last block found");
				lastBlock = true;
			}

			int BTYPE = ((data[index] & 0x6) >>> 1);
			switch (BTYPE) {
			case COMPRESSION_NONE:
				System.out.println("No compression block");
				break;
			case COMPRESSION_FIXED_HUFFMAN:
				System.out.println("Compression with fixed huffman");
				// now decode the literal/length value
				break;
			case COMPRESSION_DYNAMIC_HUFFMAN:
				System.out.println("Compression with dynamic huffman");
				break;
			case COMPRESSION_RESERVED:
				throw new IllegalStateException(
						"Reserved compression type detected");
			default:
				throw new IllegalStateException(
						"Unrecognized compression type : " + BTYPE);
			}
		} while (!lastBlock);
	}

	private static void parseIEND(byte[] data, int offset, int length) {
		// System.out.println("image end met");
	}

	public static int readInt(byte[] data, int offset) {
		return ((data[offset] & 0xff) << 24)
				| ((data[offset + 1] & 0xff) << 16)
				| ((data[offset + 2] & 0xff) << 8) | (data[offset + 3] & 0xff);
	}

	public static String readString(byte[] data, int offset, int length) {
		StringBuffer sb = new StringBuffer();
		for (int i = 0; i < length; i++) {
			sb.append((char) data[offset + i]);
		}
		return sb.toString();
	}

	public static PNGImage createPNGImage(PNGImage image) {
		return null;
	}

	public static PNGImage createPNGImage(PNGImage image, int x, int y,
			int width, int height, int transform) {
		return null;
	}

	/**
	 * Load png image from an input stream ,the whole stream will be treated as
	 * on png image
	 * 
	 * @param is
	 *            input stream to load png image
	 * 
	 * @return png image
	 */
	public static PNGImage createPNGImage(InputStream is) throws Exception {
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		byte[] buffer = new byte[1024];
		int lengthRead = -1;
		while ((lengthRead = is.read(buffer)) != -1) {
			baos.write(buffer, 0, lengthRead);
		}
		buffer = baos.toByteArray();
		return PNGImage.createPNGImage(buffer, 0, buffer.length);
	}

	public static PNGImage createPNGImage(int[] rgb, int width, int height,
			boolean processAlpha) {
		return null;
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	public void draw(Graphics g, int x, int y) {
		for (int i = 0; i < height; i++) {
			for (int j = 0; j < width; j++) {
				g.setColor(rgb[i * width + j]);
				g.fillRect(x + j, y + i, 1, 1);
			}
		}
		g.setColor(0);
		g.drawRect(x, y, width, height);
	}

	public void drawRegion(Graphics g, int srcX, int srcY, int width,
			int height, int transform, int desX, int desY, int anchor) {

	}

	private static int crc() {
		return 0;
	}

	private static int adler() {
		return 0;
	}

	public int[] getRGB() {
		return rgb;
	}

	private final static int PRECISION = 1 << 16;

	public void scale(int width, int height) {
		if (width > 0 && height > 0
				&& (!(this.width == width && this.height == height))) {
			int srcWidth = this.width;
			int srcHeight = this.height;

			int xRatio = srcWidth * PRECISION / width;
			int yRatio = srcHeight * PRECISION / height;

			int xOffset = xRatio >> 1;
			int yOffset = yRatio >> 1;

			int[] destinationRGB = new int[width * height];
			for (int i = 0; i < height; i++) {
				int srcY = yOffset / PRECISION;
				for (int j = 0; j < width; j++) {
					int srcX = xOffset / PRECISION;
					destinationRGB[i * width + j] = rgb[srcY * srcWidth + srcX];

					xOffset += xRatio;
				}

				xOffset = xRatio >> 1;
				yOffset += yRatio;
			}

			this.width = width;
			this.height = height;
			this.rgb = destinationRGB;
		}
	}
}
